package org.infoset.xml;

import java.util.*;
import java.net.*;

/**
 * This class represents a namespace scope.  An instance of this may be 
 * attached to a parent scope and represent a heirarchical chain of scopes.
 * @author <a href="mailto:alex@milowski.com">R. Alexander Milowski</a>
 */
public class NamespaceScope { 

   private static class DefaultDataProxy extends HashMap<String,URI> {
      DefaultDataProxy() {
         super.put("xml",Name.XML_NAMESPACE_URI);
      }
      
      public URI put(String prefix,URI value) {
         throw new RuntimeException("Cannot add namespace bindings to the XML data proxy.");
      }
      
      public void putAll(Map<? extends String,? extends URI> collection) {
         throw new RuntimeException("Cannot add namespace bindings to the XML data proxy.");
      }
      
      public URI remove(Object o) {
         throw new RuntimeException("Cannot remove namespace bindings to the XML data proxy.");
      }
   }
   
   private static final Map<String,URI> DEFAULT_PROXY = new DefaultDataProxy();
   
   private static final NamespaceScope defaultScope = new NamespaceScope(null, DEFAULT_PROXY);
   
   public static Map<String,URI> getDefaultPrefixes() {
      return DEFAULT_PROXY;
   }
   
   public static NamespaceScope getDefaultScope() {
      return defaultScope;
   }
   
   /**
    * An exception throw when a prefix is not declared.
    */
   public static class NotDeclared extends XMLException { 
      NotDeclared(String msg) {
         super(msg);
      }
   }

   static class InScopePrefixesIterator implements Iterator<String> {
     NamespaceScope current;
     Iterator<String> declared;
     Map prefixes;
     String theNext;
     InScopePrefixesIterator(NamespaceScope scope) {
        current = scope;
        prefixes = new HashMap();
        declared = scope.getDataProxy().keySet().iterator();
     }
     public void remove() {
        throw new RuntimeException("Cannot remove prefix from within an in-scope prefix iteration.");
     }
     public boolean hasNext() {
        if (theNext==null) {
           theNext = next();
        }
        return theNext!=null;
     }
     public String next() {
        if (theNext!=null) {
           String toReturn = theNext;
           theNext = null;
           return toReturn;
        }
        while (current!=null) {
           while (declared.hasNext()) {
              String prefix = (String)declared.next();
              if (prefixes.get(prefix)==null) {
                 prefixes.put(prefix,Boolean.TRUE);
                 return prefix;
              }
           }
           if (!declared.hasNext()) {
              current = current.getParent();
              if (current!=null) {
                 declared = current.getDataProxy().keySet().iterator();
              }
           }
        }
        return null;
     }
   }

   NamespaceScope parent;
   Map<String,URI> proxy;

  // TODO: optimize the construction of the hashtables

  // TODO: handle case where push is called after there are subscopes

   /**
    * Constructs an empty namespace scope without a parent scope.
    */
   /*
   public NamespaceScope() {
      proxy = defaultProxy;
      parent = null;
   }
    */

   /**
    * Constructs an empty namespace scope with a parent scope.
    * @param parent The parent scope.
    */
   public NamespaceScope(NamespaceScope parent,Map<String,URI> proxy) {
      this.proxy = proxy;
      if (proxy==null) {
         throw new IllegalArgumentException("The data proxy cannot be null.");
      }
      this.parent = parent;
   }

   /**
    * Returns the parent scope.
    * @return The parent namespace scope or null if there is no such scope.
    */
   public NamespaceScope getParent() {
      return parent;
   }

   /**
    * Sets the namespace scope's parent (very dangerous!).
    */
   /*
   public void setParent(NamespaceScope scope) {
      if (scope==parent) {
         throw new RuntimeException("Cannot set parent to itself!");
      }
      parent = scope;
   }
    */
   
   public Map<String,URI> getDataProxy() {
      return proxy;
   }

   /**
    * Expands a qname into a name instance.
    * @param name The qname value to expand.
    * @return A name instance.
    * @throws XMLException Throw when the prefix is not declared or the qname is invalid.
    */
   public Name expandName(String name) 
      throws XMLException
   {
      return expandName(name,false,InfosetFactory.getDefaultInfoset());
   }

   /**
    * Expands a qname into a name instance.
    * @param name The qname value to expand.
    * @param useDefault a flag indicating if the default should be used for names without prefixes.
    * @return A name instance.
    * @throws XMLException Throw when the prefix is not declared or the qname is invalid.
    */
   public Name expandName(String name,boolean useDefault) 
      throws XMLException
   {
      return expandName(name,useDefault,InfosetFactory.getDefaultInfoset());
   }
   
   public Name expandName(String name,boolean useDefault,NameTable table) 
      throws XMLException
   {
      // TODO: implement interning with Context
      int colon = name.indexOf(':');
      if (colon<0) {
         URI uri = useDefault ? getNamespace(Name.NO_PREFIX) : null;
         return uri==null ? table.createName(name) : table.createName(uri,name);
      } else if (colon==0) {
         throw new XMLException("Bad prefixed name '"+name+"' : colon at beginning.");
      } else {
         String prefix = name.substring(0,colon);
         String local = name.substring(colon+1);
         URI uri = getNamespace(prefix);
         if (uri==null) {
            throw new XMLException("Bad prefix "+prefix+": not mapped to URI.");
	 }
         return table.createName(uri,local);
      }
   }
   
   public String toQName(Name nm,boolean element) {
      URI ns = nm.getNamespaceName();
      if (!element) {
         if (ns==Name.NO_NAMESPACE) {
            return nm.getLocalName();
         } else {
            String prefix = getNearestPrefix(nm.getNamespaceName());
            return prefix==null ? null : prefix+':'+nm.getLocalName();
         }
         /*
      } else if (ns==null) {
         if (getNamespace(Name.NO_PREFIX)==null) {
            String prefix = getNearestPrefix(nm.getNamespaceName());
            return prefix==null ? null : prefix+':'+nm.getLocalName();
         } else {
            return null;
         }*/
      } else if (ns==Name.NO_NAMESPACE) {
         return nm.getLocalName();
      } else {
         String prefix = getNearestPrefix(ns);
         if (prefix==Name.NO_PREFIX && ns!=Name.NO_NAMESPACE) {
            return '{'+ns.toString()+'}'+nm.getLocalName();
         } else {
            return prefix==null ? null : prefix+':'+nm.getLocalName();
         }
      }
   }
 
   /**
    * Sets the namespace scope's parent.
    * @param scope The new parent.
    */
   /*
   public void adoptBy(NamespaceScope scope) {      
      parent = scope;
   }
    */

   /**
    * Returns all the in-scope prefixes.
    * @return An interation of string prefix values.
    */
   public Iterator getPrefixes() {
      return new InScopePrefixesIterator(this);
   }

   /**
    * Returns an in-scope namespace name mapping for this prefix.  This may include mappings from parent scopes.
    * @param prefix The prefix string with which to search.
    * @return A namespace name or null if there is no mapping.
    */
   public URI getNamespace(String prefix) {
//      if (proxy.isPrefixExcluded(prefix)) {
//         return null;
//      }
      URI ns = proxy.get(prefix);
      if (ns==null && parent!=null) {
         ns = parent.getNamespace(prefix);
      }
      return ns;
   }

   /**
    * Returns the nearest in-scope prefix for a namespace name.  This may include mappings from parent scopes.
    * @param namespace The namespace with which to search.
    * @return A prefix value or null if there is no mapping.
    */
   public String getNearestPrefix(URI namespace) {

      for (Map.Entry<String,URI> entry : proxy.entrySet()) {
         if (entry.getValue().equals(namespace)) {
            return entry.getKey();
         }
      }
      String prefix = null;
      if (parent!=null) {
         prefix = parent.getNearestPrefix(namespace);
         if (prefix!=null && proxy.get(prefix)!=null) {
            prefix = null;
         }
      }
      return prefix;
   }

   /**
    * Returns the appropriate prefix to use with the specified name.  This 
    * prefix value represents the nearest in-scope prefix or null to signify 
    * that no prefix value should be used as the result of the name being 
    * unqualified by a namespace name or that an appropriate default namespace
    * is already in scope.
    * @param name The name to use in the search.
    * @return A prefix value or null if no prefix should be used.
    * @throws NotDeclared Thrown when a namespace name is specified on the name and there is no in-scope prefix or default namespace name that matches the URI value.
    */
   public String getPrefix(Name name) 
      throws NotDeclared
   {
     //System.out.println("prefix for "+name);
      URI uri = name.getNamespaceName();

      //System.out.println("uri = '"+uri+"'");
      
      //if (uri==Name.NO_NAMESPACE) {
      //   return Name.NO_PREFIX;
      //}

      String prefix = getNearestPrefix(uri);

      if (prefix==null) {
         throw new NotDeclared("Prefix not declarted for namespace "+uri);
      }
      return prefix;
   }
}
